const MIN_SCALE = 0.5;
const MAX_SCALE = 4;

const EASE = (t) => t<.5 ? 2*t*t : -1+(4-2*t)*t; // easeInOutQuad

function nowTs(){ return Date.now(); }

Page({
  data: {
    // 地图资源（原图尺寸会在 fetchMapSize 中获取）
    mapUrl: 'https://nzy.gonng.com/storage/mini/map.png',
    mapWidth: 1415,
    mapHeight: 1258,

    // 数据
    chapterList: [],
    exhibitList: [],
    selectedChapter: null,

    // 弹窗
    showContentModal: false,
    currentExhibitContent: '',
    currentExhibitName: '',
    clickedExhibitId: null,

    // 目录弹窗
    showCatalogModal: false,
    expandedChapters: {},

    // 音频
    isAudioPlaying: false,
    currentAudioExhibitId: null
  },

  onLoad() {
    this.fetchMapSize();
    this.getExhibitList();
    this.initInnerAudio();
  },

  onReady() {
    this.initCanvas();
  },

  // 读取地图原始尺寸
  fetchMapSize() {
    wx.getImageInfo({
      src: this.data.mapUrl,
      success: (res) => {
        this.setData({ mapWidth: res.width, mapHeight: res.height });
        // 如果画布已初始化且图片已加载，重新设置初始变换
        if (this.canvas && this.mapImage && this.mapImage.complete) {
          this.setInitialTransform();
          this.requestDraw();
        }
      },
      fail: (res) => {}
    });
  },

  // 初始化 Canvas（2D）
  initCanvas() {
    const query = wx.createSelectorQuery();
    query.select('#mapCanvas').fields({ node: true, size: true }).exec((res) => {
      if (!res || !res[0]) return;
      const canvas = res[0].node;
      const { width, height } = res[0];
      const dpr = wx.getSystemInfoSync().pixelRatio || 1;
      canvas.width = width * dpr;
      canvas.height = height * dpr;
      const ctx = canvas.getContext('2d');
      ctx.scale(dpr, dpr); // 之后按 CSS 像素作图

      this.canvas = canvas;
      this.ctx = ctx;
      this.dpr = dpr;
      this.canvasW = width;
      this.canvasH = height;

      // 加载地图图片
      const img = canvas.createImage();
      img.onload = () => {
        this.mapImage = img;
        this.setInitialTransform();
        this.requestDraw();
      };
      img.src = this.data.mapUrl;
    });
  },

  // 设置初始变换：不超过原始比例的等比适配（默认不放大原图）
  setInitialTransform() {
    const W = this.canvasW || 0;
    const H = this.canvasH || 0;
    const mw = this.data.mapWidth;
    const mh = this.data.mapHeight;
    if (!W || !H || !mw || !mh) return;

    let fit = Math.min(W / mw, H / mh) * 0.95;
    if (!isFinite(fit) || fit <= 0) fit = 1;
    fit = Math.min(1, fit);

    this.fitScale = fit; // 记录适配最小缩放
    this.scale = fit;
    this.translateX = (W - mw * fit) / 2;
    this.translateY = (H - mh * fit) / 2;
  },

  // 将世界坐标(原图像素)转换为屏幕坐标
  worldToScreen(wx, wy) {
    return {
      x: wx * this.scale + this.translateX,
      y: wy * this.scale + this.translateY
    };
  },

  // 屏幕坐标到世界坐标
  screenToWorld(sx, sy) {
    return {
      x: (sx - this.translateX) / this.scale,
      y: (sy - this.translateY) / this.scale
    };
  },

  // 重新绘制
  draw() {
    if (!this.ctx || !this.canvasW || !this.canvasH) return;
    const ctx = this.ctx;
    ctx.clearRect(0, 0, this.canvasW, this.canvasH);

    // 绘制底图
    if (this.mapImage) {
      ctx.save();
      ctx.setTransform(this.dpr || 1, 0, 0, this.dpr || 1, 0, 0); // 重置
      // 使用变换手动控制缩放和平移
      ctx.translate(this.translateX, this.translateY);
      ctx.scale(this.scale, this.scale);
      ctx.drawImage(this.mapImage, 0, 0, this.data.mapWidth, this.data.mapHeight);
      ctx.restore();
    }

    // 可视范围裁剪（提高性能）
    const W = this.canvasW, H = this.canvasH;
    const minWX = (0 - this.translateX) / this.scale;
    const minWY = (0 - this.translateY) / this.scale;
    const maxWX = (W - this.translateX) / this.scale;
    const maxWY = (H - this.translateY) / this.scale;
    const padW = 40 / this.scale; // 视口外预留一定 padding
    const padH = 40 / this.scale;

    // 绘制标记（用屏幕坐标，保证圆点大小稳定）
    const list = this.data.exhibitList || [];
    const selectedId = this.data.clickedExhibitId;
    for (let i = 0; i < list.length; i++) {
      const item = list[i] || {};
      const mx = Number(item.coordinates_x) || 0;
      const my = Number(item.coordinates_y) || 0;
      if (mx < minWX - padW || mx > maxWX + padW || my < minWY - padH || my > maxWY + padH) continue;
      const p = this.worldToScreen(mx, my);
      const highlight = selectedId && item.id === selectedId;
      this.drawMarker(p.x, p.y, item.name, highlight);
    }
  },

  // 批处理重绘，避免 touchmove 频繁重绘卡顿
  requestDraw() {
    if (this._rafScheduled) return;
    this._rafScheduled = true;
    const step = () => {
      this._rafScheduled = false;
      this.draw();
    };
    if (this.canvas && typeof this.canvas.requestAnimationFrame === 'function') this.canvas.requestAnimationFrame(step);
    else setTimeout(step, 16);
  },

  drawMarker(sx, sy, name, highlight) {
    const ctx = this.ctx;
    const r = highlight ? 7.5 : 6; // 高亮时略大
    ctx.save();

    if (highlight) {
      // 外环高亮
      ctx.beginPath();
      ctx.arc(sx, sy, r + 5, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(255,77,79,0.18)';
      ctx.fill();
      ctx.closePath();
    }

    ctx.beginPath();
    ctx.arc(sx, sy, r, 0, Math.PI * 2);
    ctx.fillStyle = '#ff4d4f';
    ctx.fill();
    ctx.lineWidth = 2;
    ctx.strokeStyle = '#fff';
    ctx.stroke();
    ctx.closePath();

    // 标签：高亮的常驻，其他的总是显示
    if (name) {
      const alpha = 1; // 总是显示
      ctx.font = '12px sans-serif';
      const paddingX = 6;
      const paddingY = 4;
      const textW = ctx.measureText(name).width;
      const boxW = textW + paddingX * 2;
      const boxH = 18 + paddingY * 2;
      const boxX = sx - boxW / 2;
      const boxY = sy + 10;

      // 背景条
      ctx.save();
      ctx.globalAlpha = 0.75 * alpha;
      ctx.fillStyle = 'rgba(0,0,0,1)';
      ctx.fillRect(boxX, boxY, boxW, boxH);
      ctx.restore();

      // 文字
      ctx.save();
      ctx.globalAlpha = alpha;
      ctx.fillStyle = '#fff';
      ctx.textBaseline = 'alphabetic';
      ctx.fillText(name, boxX + paddingX, boxY + paddingY + 14);
      ctx.restore();
    }
    ctx.restore();
  },

  // ============== Touch 交互：拖拽/缩放/点击命中 ==============
  onTouchStart(e) {
    // 新触摸开始，取消可能存在的惯性动画
    this._inertiaActive = false;
    // 若有正在进行的过渡动画，停止它，避免与手势冲突
    this._animating = false;

    const t = e.touches || [];
    if (t.length === 1) {
      this.isDragging = true;
      this.isPinching = false;
      this.startX = t[0].x;
      this.startY = t[0].y;
      this.lastX = this.startX;
      this.lastY = this.startY;
      this.tapStartTime = Date.now();
      this.tapStartPos = { x: this.startX, y: this.startY };
      // 速度跟踪
      this.vx = 0; this.vy = 0; this.lastMoveTime = Date.now();
    } else if (t.length >= 2) {
      this.isDragging = false;
      this.isPinching = true;
      this.startTouchesDistance = this.getDistance(t[0], t[1]);
      this.startScale = this.scale;
      // 记录缩放中心（屏幕坐标）
      this.pinchCenter = {
        x: (t[0].x + t[1].x) / 2,
        y: (t[0].y + t[1].y) / 2
      };
    }
  },

  onTouchMove(e) {
    const t = e.touches || [];
    if (this.isDragging && t.length === 1) {
      const dx = t[0].x - this.lastX;
      const dy = t[0].y - this.lastY;
      this.translateX += dx;
      this.translateY += dy;

      // 更新速度
      const now = Date.now();
      const dt = Math.max(1, now - (this.lastMoveTime || now));
      this.vx = (t[0].x - this.lastX) / dt;
      this.vy = (t[0].y - this.lastY) / dt;
      this.lastMoveTime = now;

      this.lastX = t[0].x;
      this.lastY = t[0].y;
      this.checkBounds();
      this.requestDraw();
    } else if (this.isPinching && t.length >= 2) {
      const newDist = this.getDistance(t[0], t[1]);
      if (!this.startTouchesDistance) return;
      let newScale = this.startScale * (newDist / this.startTouchesDistance);
      const minScale = this.getMinScale();
      newScale = Math.max(minScale, Math.min(MAX_SCALE, newScale));
      const k = newScale / this.scale;
      const cx = this.pinchCenter.x;
      const cy = this.pinchCenter.y;
      // 缩放围绕 pinch 中心
      this.translateX = cx - (cx - this.translateX) * k;
      this.translateY = cy - (cy - this.translateY) * k;
      this.scale = newScale;
      this.checkBounds();
      this.requestDraw();
    }
  },

  onTouchEnd(e) {
    // 处理点击/双击/惯性
    if (this.isDragging && !this.isPinching) {
      const now = Date.now();
      const duration = now - (this.tapStartTime || now);
      const endPos = this.lastX != null ? { x: this.lastX, y: this.lastY } : this.tapStartPos;
      const moveDist = Math.hypot((endPos.x - this.tapStartPos.x) || 0, (endPos.y - this.tapStartPos.y) || 0);
      const isTap = duration < 250 && moveDist < 8;
      if (isTap) {
        // 双击检测
        const lastTime = this._lastTapTime || 0;
        const lastPos = this._lastTapPos || endPos;
        const dt = now - lastTime;
        const dd = Math.hypot(endPos.x - (lastPos.x || endPos.x), endPos.y - (lastPos.y || endPos.y));
        if (dt < 300 && dd < 20) {
          // 双击：放大或重置
          if (this.scale < 1.2) {
            this.zoomBy(1.6, endPos.x, endPos.y, 200);
          } else {
            const fit = this.getFitTransform();
            this.animateToTransform(fit.scale, fit.translateX, fit.translateY, 220);
          }
          this._lastTapTime = 0; // 重置，避免三连击
          this._lastTapPos = endPos;
        } else {
          // 单击：命中检测
          const hit = this.hitTest(endPos.x, endPos.y);
          if (hit) {
            this.openExhibit(hit);
          }
          this._lastTapTime = now;
          this._lastTapPos = endPos;
        }
      } else {
        // 惯性滚动（速度阈值）
        const speed = Math.hypot(this.vx || 0, this.vy || 0);
        if (speed > 0.2) {
          this.startInertia(this.vx, this.vy);
        }
      }
    }

    this.isDragging = false;
    this.isPinching = false;
    this.startTouchesDistance = 0;
  },

  onTouchCancel(e) {
    // 取消手势，停止惯性
    this._inertiaActive = false;
    this.isDragging = false;
    this.isPinching = false;
    this.startTouchesDistance = 0;
  },

  startInertia(vx, vy) {
    // 基于像素/毫秒的速度，简单指数衰减
    this._inertiaActive = true;
    let ivx = vx, ivy = vy;
    const friction = 0.95;
    let last = Date.now();
    const step = () => {
      if (!this._inertiaActive) return;
      const now = Date.now();
      const dt = Math.min(32, Math.max(16, now - last)); // 16~32ms之间
      last = now;

      // 速度衰减
      ivx *= friction;
      ivy *= friction;

      // 位移积分
      let nx = this.translateX + ivx * dt;
      let ny = this.translateY + ivy * dt;

      // 约束并在触边时衰减对应速度
      const clamped = this.clampTransform(this.scale, nx, ny);
      if (Math.abs(clamped.translateX - nx) > 0.1) ivx *= 0.3;
      if (Math.abs(clamped.translateY - ny) > 0.1) ivy *= 0.3;
      this.translateX = clamped.translateX;
      this.translateY = clamped.translateY;

      this.requestDraw();

      // 停止条件：速度很小
      if (Math.hypot(ivx, ivy) < 0.02) {
        this._inertiaActive = false;
        return;
      }
      if (this.canvas && typeof this.canvas.requestAnimationFrame === 'function') this.canvas.requestAnimationFrame(step);
      else setTimeout(step, 16);
    };
    step();
  },

  getDistance(a, b) {
    return Math.hypot(a.x - b.x, a.y - b.y);
  },

  hitTest(sx, sy) {
    const list = this.data.exhibitList || [];
    if (!this.ctx) return null;

    let nearestDot = null;
    let minDotDist = Infinity;

    // Iterate backwards because things drawn later are on top
    for (let i = list.length - 1; i >= 0; i--) {
      const item = list[i] || {};
      const mx = Number(item.coordinates_x) || 0;
      const my = Number(item.coordinates_y) || 0;
      const p = this.worldToScreen(mx, my);

      // Check for label hit first
      if (item.name) {
        this.ctx.font = '12px sans-serif';
        const paddingX = 6;
        const paddingY = 4;
        const textW = this.ctx.measureText(item.name).width;
        const boxW = textW + paddingX * 2;
        const boxH = 18 + paddingY * 2;
        const boxX = p.x - boxW / 2;
        const boxY = p.y + 10;

        if (sx >= boxX && sx <= boxX + boxW && sy >= boxY && sy <= boxY + boxH) {
          return item; // Direct hit on a label, this is our target
        }
      }

      // If no label hit, check for dot hit and find the nearest
      const d = this.getDistance({ x: sx, y: sy }, p);
      if (d < 18) { // 18px touch radius for dot
        if (d < minDotDist) {
          minDotDist = d;
          nearestDot = item;
        }
      }
    }

    return nearestDot; // Return nearest dot if no label was hit
  },

  zoomIn() {
    this.zoomBy(1.2, this.canvasW/2, this.canvasH/2, 180);
  },
  zoomOut() {
    this.zoomBy(1/1.2, this.canvasW/2, this.canvasH/2, 180);
  },
  resetFit() {
    const fit = this.getFitTransform();
    this.animateToTransform(fit.scale, fit.translateX, fit.translateY, 220);
  },
  zoomBy(factor, cx, cy, duration = 0) {
    const minScale = this.getMinScale();
    const targetScale = Math.max(minScale, Math.min(MAX_SCALE, this.scale * factor));
    const k = targetScale / this.scale;
    const tx = cx - (cx - this.translateX) * k;
    const ty = cy - (cy - this.translateY) * k;
    const clamped = this.clampTransform(targetScale, tx, ty);
    if (duration && duration > 0) {
      this.animateToTransform(clamped.scale, clamped.translateX, clamped.translateY, duration);
    } else {
      this.scale = clamped.scale;
      this.translateX = clamped.translateX;
      this.translateY = clamped.translateY;
      this.requestDraw();
    }
  },

  animateToTransform(targetScale, targetX, targetY, duration = 260, ease = 'easeOutCubic', onComplete) {
    // 开启动画时，停止惯性，并标记当前处于动画中
    this._inertiaActive = false;
    this._animating = true;

    const startS = this.scale, startX = this.translateX, startY = this.translateY;
    const ds = targetScale - startS, dx = targetX - startX, dy = targetY - startY;
    const startTime = Date.now();

    const easing = (t) => {
      if (ease === 'linear') return t;
      // easeOutCubic
      return 1 - Math.pow(1 - t, 3);
    };

    const step = () => {
      // 若已被外部打断（如手势开始），立即终止动画
      if (!this._animating) return;
      const now = Date.now();
      const t = Math.min(1, (now - startTime) / duration);
      const et = easing(t);
      const minScale = this.getMinScale();
      const nsRaw = startS + ds * et;
      const ns = Math.max(minScale, Math.min(MAX_SCALE, nsRaw));
      const nx = startX + dx * et;
      const ny = startY + dy * et;
      const clamped = this.clampTransform(ns, nx, ny);
      this.scale = clamped.scale;
      this.translateX = clamped.translateX;
      this.translateY = clamped.translateY;
      this.requestDraw();
      if (t < 1) {
        if (this.canvas && typeof this.canvas.requestAnimationFrame === 'function') this.canvas.requestAnimationFrame(step);
        else setTimeout(step, 16);
      } else {
        this._animating = false;
        if (typeof onComplete === 'function') onComplete();
      }
    };
    step();
  },

  centerOnWorld(wx, wy, scale, duration=220, onComplete) {
    const minScale = this.getMinScale();
    const targetScale = Math.max(minScale, Math.min(MAX_SCALE, (scale || this.scale)));
    const cx = this.canvasW/2, cy = this.canvasH/2;
    const tx = cx - wx * targetScale; const ty = cy - wy * targetScale;
    const clamped = this.clampTransform(targetScale, tx, ty);
    this.animateToTransform(clamped.scale, clamped.translateX, clamped.translateY, duration, 'easeOutCubic', onComplete);
  },

  openExhibit(item) {
    // 动画居中展项后，再显示弹窗
    const mx = Number(item.coordinates_x) || 0; const my = Number(item.coordinates_y) || 0;
    this.setData({ clickedExhibitId: item && item.id != null ? String(item.id) : null });

    // 兼容新接口：resource(s)/item[] 中 type===2 为音频
    const audioUrl = this.extractAudioUrlFromResources(item && (item.resource || item.resources || item.item)) || this.extractAudioUrl(item);

    this.centerOnWorld(mx, my, this.scale, 200, () => {
      this.setData({
        showContentModal: true,
        currentExhibitContent: item.content || '暂无介绍',
        currentExhibitName: item.name || '',
        currentExhibitAudioUrl: audioUrl || null,
      });
    });
  },
  // 保持地图不越界（简单边界约束）
  checkBounds() {
    const W = this.canvasW || 0;
    const H = this.canvasH || 0;
    const mw = this.data.mapWidth;
    const mh = this.data.mapHeight;
    if (!W || !H || !mw || !mh) return;

    const scaledW = mw * this.scale;
    const scaledH = mh * this.scale;

    if (scaledW <= W) {
      // 图小于画布：X 居中锁定
      this.translateX = (W - scaledW) / 2;
    } else {
      const minX = W - scaledW; // 负值
      const maxX = 0;
      if (this.translateX < minX) this.translateX = minX;
      if (this.translateX > maxX) this.translateX = maxX;
    }

    if (scaledH <= H) {
      // 图小于画布：Y 居中锁定
      this.translateY = (H - scaledH) / 2;
    } else {
      const minY = H - scaledH; // 负值
      const maxY = 0;
      if (this.translateY < minY) this.translateY = minY;
      if (this.translateY > maxY) this.translateY = maxY;
    }
  },

  // 动态最小缩放：不小于初始自适应比例，最多不超过原图 1:1
  getMinScale() {
    const W = this.canvasW || 0;
    const H = this.canvasH || 0;
    const mw = this.data.mapWidth;
    const mh = this.data.mapHeight;
    // 优先使用 setInitialTransform 记录的适配比例
    let fit = this.fitScale;
    if (!fit) {
      if (!W || !H || !mw || !mh) return MIN_SCALE;
      fit = Math.min(W / mw, H / mh) * 0.95;
      if (!isFinite(fit) || fit <= 0) fit = 1;
      fit = Math.min(1, fit);
    }
    return fit;
  },

  // 计算“自适应居中”的变换
  getFitTransform() {
    const W = this.canvasW || 0;
    const H = this.canvasH || 0;
    const mw = this.data.mapWidth;
    const mh = this.data.mapHeight;
    let fit = Math.min(W / mw, H / mh) * 0.95;
    if (!isFinite(fit) || fit <= 0) fit = 1;
    fit = Math.min(1, fit);
    const tx = (W - mw * fit) / 2;
    const ty = (H - mh * fit) / 2;
    return { scale: fit, translateX: tx, translateY: ty };
  },

  // 约束到边界，返回合法的 scale 与位移（当图小于画布时锁定居中）
  clampTransform(scale, tx, ty) {
    const W = this.canvasW || 0;
    const H = this.canvasH || 0;
    const mw = this.data.mapWidth;
    const mh = this.data.mapHeight;

    const minScale = this.getMinScale();
    const s = Math.max(minScale, Math.min(MAX_SCALE, scale));

    const scaledW = mw * s;
    const scaledH = mh * s;

    let nx = tx;
    let ny = ty;

    if (scaledW <= W) {
      nx = (W - scaledW) / 2;
    } else {
      const minX = W - scaledW; // 负值
      const maxX = 0;
      nx = Math.max(minX, Math.min(maxX, nx));
    }

    if (scaledH <= H) {
      ny = (H - scaledH) / 2;
    } else {
      const minY = H - scaledH; // 负值
      const maxY = 0;
      ny = Math.max(minY, Math.min(maxY, ny));
    }

    return { scale: s, translateX: nx, translateY: ny };
  },


  // 弹窗关闭
  closeContentModal() {
    const { isAudioPlaying, currentAudioExhibitId, clickedExhibitId } = this.data;
    if (isAudioPlaying && currentAudioExhibitId && currentAudioExhibitId === clickedExhibitId) {
      this.stopExplainAudio();
    }
    this.setData({ showContentModal: false, currentExhibitContent: '', currentExhibitName: '', clickedExhibitId: null });
  },

  // 查看详情
  handleDetailTap() {
    const id = this.data.clickedExhibitId;
    if (!id) { this.closeContentModal(); return; }
    this.closeContentModal();
    wx.navigateTo({ url: `/pages/exhibition/exhibit?exhibitId=${id}` });
  },

  // 解说按钮
  handleExplainTap() {
    const id = this.data.clickedExhibitId;
    const idStr = id != null ? String(id) : '';
    let url = this.data.currentExhibitAudioUrl;

    if (!idStr) { this.closeContentModal(); return; }

    // 若当前未缓存到音频，尝试从 exhibitList 中重新提取一次
    if (!url) {
      const item = (this.data.exhibitList || []).find(x => x && String(x.id) === idStr);
      if (item) {
        url = this.extractAudioUrlFromResources(item.resource || item.resources || item.item) || this.extractAudioUrl(item);
        if (url) this.setData({ currentExhibitAudioUrl: url });
      }
    }

    if (this.data.isAudioPlaying && String(this.data.currentAudioExhibitId) === idStr) { this.stopExplainAudio(); return; }

    if (!url) { wx.showToast({ title: '暂无音频', icon: 'none' }); return; }

    this.playExplainAudio(url, idStr);
  },

  // ============ 音频播放封装 ============
  initInnerAudio() {
    if (this.innerAudio) return;
    const innerAudio = wx.createInnerAudioContext();
    innerAudio.obeyMuteSwitch = false; innerAudio.autoplay = false;
    innerAudio.onPlay(() => {
      this.setData({
        isAudioPlaying: true,
        currentAudioExhibitId: this._pendingAudioId != null ? String(this._pendingAudioId) : this.data.currentAudioExhibitId
      });
      this._pendingAudioId = null;
    });
    innerAudio.onPause(() => { this.setData({ isAudioPlaying: false }); });
    innerAudio.onStop(() => {
      if (this._ignoreNextStop) { this._ignoreNextStop = false; return; }
      this.setData({ isAudioPlaying: false, currentAudioExhibitId: null });
    });
    innerAudio.onEnded(() => { this.setData({ isAudioPlaying: false, currentAudioExhibitId: null }); });
    innerAudio.onError(() => { this.setData({ isAudioPlaying: false }); wx.showToast({ title: '音频播放失败', icon: 'none' }); });
    this.innerAudio = innerAudio;
  },
  playExplainAudio(url, id) {
    this.initInnerAudio();
    this._pendingAudioId = String(id);
    this._ignoreNextStop = true;
    try { this.innerAudio.stop(); } catch(e) {}
    this.innerAudio.src = url; this.innerAudio.play();
    this.setData({ isAudioPlaying: true, currentAudioExhibitId: String(id) });
  },
  stopExplainAudio() {
    if (!this.innerAudio) return; try { this.innerAudio.stop(); } catch(e) {}
    this.setData({ isAudioPlaying: false, currentAudioExhibitId: null });
  },
  extractAudioUrl(item) {
    if (!item) return '';
    const raw = item || {};
    const content = raw.content || '';
    const candidates = [raw.audio_url, raw.audio, raw.voice_url, raw.voice, raw.sound, raw.mp3, raw.audioUrl, raw.voiceUrl].filter(Boolean);
    if (candidates.length) return this.normalizeUrl(candidates[0]);
    try {
      const m1 = /<audio[^>]*src=["']([^"']+)["'][^>]*>/i.exec(content);
      if (m1 && m1[1]) return this.normalizeUrl(m1[1]);
      const m2 = /(https?:\/\/[^\s'\"]+\.(mp3|m4a|aac|wav))/i.exec(content);
      if (m2 && m2[1]) return this.normalizeUrl(m2[1]);
    } catch(e) {}
    return '';
  },

  // 从 resource 列表中提取音频URL（type===2）
  extractAudioUrlFromResources(resources) {
    console.log('extractAudioUrlFromResources', resources);
    let list = resources;
    if (typeof list === 'string') {
      try { list = JSON.parse(list); } catch(e) { list = []; }
    }
    if (!Array.isArray(list)) return '';

    const pickUrl = (val) => {
      if (!val) return '';
      if (typeof val === 'string') return this.normalizeUrl(val);
      if (Array.isArray(val) && val.length) return this.normalizeUrl(val[0]);
      if (typeof val === 'object') {
        const keys = ['url','full_url','oss_url','download_url','cdn_url','file_url','fileUrl','file_path','filePath','path','src','link'];
        for (let k of keys) { if (val[k]) return this.normalizeUrl(val[k]); }
      }
      return '';
    };

    for (let i = 0; i < list.length; i++) {
      const r = list[i] || {};
      if (Number(r.type) === 2) {
        // 常规直接字段
        let u = pickUrl(r);
        if (u) return u;
        // 尝试嵌套对象
        const nests = [r.file, r.media, r.asset, r.data];
        for (let n of nests) { const u2 = pickUrl(n); if (u2) return u2; }
        // 尝试从内容中提取
        const content = r.content || r.html || r.text || '';
        try {
          const m1 = /<audio[^>]*src=["']([^"']+)["'][^>]*>/i.exec(content);
          if (m1 && m1[1]) return this.normalizeUrl(m1[1]);
          const m2 = /(https?:\/\/[^\s'\"]+\.(mp3|m4a|aac|wav))/i.exec(content);
          if (m2 && m2[1]) return this.normalizeUrl(m2[1]);
        } catch(e) {}
      }
    }
    return '';
  },

  // 解析基础域名，用于将相对路径转为绝对URL
  getBaseOrigin() {
    const mapUrl = this.data.mapUrl || '';
    const m = /^https?:\/\/[^/]+/i.exec(mapUrl);
    if (m && m[0]) return m[0];
    // 兜底域名（与地图资源同域）
    return 'https://nzy-api.congqian.cn';
  },
  // 规范化URL：支持 http(s)、协议相对、以及相对路径
  normalizeUrl(url) {
    if (!url || typeof url !== 'string') return '';
    url = url.trim();
    // 去掉包裹在外层的引号或反引号
    url = url.replace(/^[`'"]+|[`'\"]+$/g, '');
    if (!url) return '';
    if (/^https?:\/\//i.test(url)) return url;
    if (/^\/\//.test(url)) return 'https:' + url;
    if (url[0] === '/') return this.getBaseOrigin() + url;
    return this.getBaseOrigin() + '/' + url.replace(/^\.\//, '');
  },

  // 获取展品列表
  getExhibitList() {
    const apiUrl = 'https://nzy-api.congqian.cn/Api/admin/exhibit_one/get_exhibit_list';
    wx.request({
      url: apiUrl,
      method: 'GET',
      header: { 'Content-Type': 'application/json' },
      success: (res) => {
        if (res.data && res.data.code === 200) {
          const chapterList = res.data.data.one || [];
          let exhibitList = [];
          let sign = true;
          for (let i = 0; i < chapterList.length; i++) {
            chapterList[i].icon = chapterList[i].item[0].url || '';
            // 把第一个有子级的篇章下的展项展示出来
            if (sign && chapterList[i].children && chapterList[i].children.length) {
              exhibitList = chapterList[i].children;
              sign = false;
            }
          }
          this.setData({ chapterList, exhibitList, selectedChapter: chapterList[0] ? chapterList[0].id : null });
          this.requestDraw();
        }
      }
    })
  },

  // 章节选择
  handleChapterClick(e) {
    const id = e.currentTarget.dataset.id;
    const chapter = (this.data.chapterList || []).find(c => c.id === id);
    if (!chapter) return;
    this.setData({ exhibitList: chapter.children || [], selectedChapter: id });
    this.requestDraw();
  },

  // 弹窗关闭
  closeContentModal() {
    const { isAudioPlaying, currentAudioExhibitId, clickedExhibitId } = this.data;
    if (isAudioPlaying && currentAudioExhibitId && currentAudioExhibitId === clickedExhibitId) {
      this.stopExplainAudio();
    }
    this.setData({ showContentModal: false, currentExhibitContent: '', currentExhibitName: '', clickedExhibitId: null });
  },

  // 查看详情
  handleDetailTap() {
    const id = this.data.clickedExhibitId;
    if (!id) { this.closeContentModal(); return; }
    this.closeContentModal();
    wx.navigateTo({ url: `/pages/exhibition/exhibit?exhibitId=${id}` });
  },

  // 解说按钮
  handleExplainTap() {
    
    const id = this.data.clickedExhibitId;
    const idStr = id != null ? String(id) : '';
    let url = this.data.currentExhibitAudioUrl;

    if (!idStr) { this.closeContentModal(); return; }
    console.log('handleExplainTap', idStr, url);
    // 若当前未缓存到音频，尝试从 exhibitList 中重新提取一次
    if (!url) {
      const item = (this.data.exhibitList || []).find(x => x && String(x.id) === idStr);
      console.log('handleExplainTap', item);
      if (item) {
        url = this.extractAudioUrlFromResources(item.resource || item.resources || item.item) || this.extractAudioUrl(item);
        if (url) this.setData({ currentExhibitAudioUrl: url });
      }
    }

    if (this.data.isAudioPlaying && String(this.data.currentAudioExhibitId) === idStr) { this.stopExplainAudio(); return; }

    if (!url) { wx.showToast({ title: '暂无音频', icon: 'none' }); return; }

    this.playExplainAudio(url, idStr);
  },

  // ============ 音频播放封装 ============
  initInnerAudio() {
    if (this.innerAudio) return;
    const innerAudio = wx.createInnerAudioContext();
    innerAudio.obeyMuteSwitch = false; innerAudio.autoplay = false;
    innerAudio.onPlay(() => {
      this.setData({
        isAudioPlaying: true,
        currentAudioExhibitId: this._pendingAudioId != null ? String(this._pendingAudioId) : this.data.currentAudioExhibitId
      });
      this._pendingAudioId = null;
    });
    innerAudio.onPause(() => { this.setData({ isAudioPlaying: false }); });
    innerAudio.onStop(() => {
      if (this._ignoreNextStop) { this._ignoreNextStop = false; return; }
      this.setData({ isAudioPlaying: false, currentAudioExhibitId: null });
    });
    innerAudio.onEnded(() => { this.setData({ isAudioPlaying: false, currentAudioExhibitId: null }); });
    innerAudio.onError(() => { this.setData({ isAudioPlaying: false }); wx.showToast({ title: '音频播放失败', icon: 'none' }); });
    this.innerAudio = innerAudio;
  },
  playExplainAudio(url, id) {
    this.initInnerAudio();
    this._pendingAudioId = String(id);
    try { this.innerAudio.stop(); } catch(e) {}
    this.innerAudio.src = url; this.innerAudio.play();
    this.setData({ isAudioPlaying: true, currentAudioExhibitId: String(id) });
  },
  stopExplainAudio() {
    if (!this.innerAudio) return; try { this.innerAudio.stop(); } catch(e) {}
    this.setData({ isAudioPlaying: false, currentAudioExhibitId: null });
  },
  extractAudioUrl(item) {
    if (!item) return '';
    const raw = item || {};
    const content = raw.content || '';
    const candidates = [raw.audio_url, raw.audio, raw.voice_url, raw.voice, raw.sound, raw.mp3, raw.audioUrl, raw.voiceUrl].filter(Boolean);
    if (candidates.length) return this.normalizeUrl(candidates[0]);
    try {
      const m1 = /<audio[^>]*src=["']([^"']+)["'][^>]*>/i.exec(content);
      if (m1 && m1[1]) return this.normalizeUrl(m1[1]);
      const m2 = /(https?:\/\/[^\s'\"]+\.(mp3|m4a|aac|wav))/i.exec(content);
      if (m2 && m2[1]) return this.normalizeUrl(m2[1]);
    } catch(e) {}
    return '';
  },

  // 从 resource 列表中提取音频URL（type===2）
  extractAudioUrlFromResources(resources) {
    if (!Array.isArray(resources)) return '';
    for (let i = 0; i < resources.length; i++) {
      const r = resources[i] || {};
      if (Number(r.type) === 2) {
        const url = r.url || r.path || r.src || r.file || r.file_url || r.filePath || r.link;
        if (url) return this.normalizeUrl(url);
      }
    }
    return '';
  },

  // 顶部语音导览入口
  goAudioGuide() {
    wx.navigateTo({ url: '/pages/audio_guide/audio_guide' });
  },

  // 打开目录
  openCatalog() {
    // 默认展开当前选中的篇章
    const expanded = { ...(this.data.expandedChapters || {}) };
    const cid = this.data.selectedChapter;
    if (cid != null) {
      expanded[cid] = true;
    }
    this.setData({ showCatalogModal: true, expandedChapters: expanded });
  },

  // 关闭目录
  closeCatalogModal() {
    this.setData({ showCatalogModal: false });
  },

  // 切换篇章展开/收起
  toggleChapterExpand(e) {
    const id = e.currentTarget.dataset.id;
    const expanded = { ...(this.data.expandedChapters || {}) };
    expanded[id] = !expanded[id];
    this.setData({ expandedChapters: expanded });
  },

  // 从目录中选择展项
  selectCatalogExhibit(e) {
    const exhibitId = e.currentTarget.dataset.id;
    const idStr = String(exhibitId);
    let target = null;
    const chapters = this.data.chapterList || [];
    for (let i = 0; i < chapters.length; i++) {
      const children = chapters[i]?.children || [];
      const found = children.find(x => String(x?.id) === idStr);
      if (found) { target = found; break; }
    }
    if (target) {
      this.setData({ showCatalogModal: false });
      this.openExhibit(target);
    }
  },

  preventBubble() {},

  onUnload() {
    if (this.innerAudio) {
      try { this.innerAudio.destroy && this.innerAudio.destroy(); } catch(e) {}
      this.innerAudio = null;
    }
  }
});